package project.common;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;

import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.mail.Authenticator;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeUtility;

import org.apache.logging.log4j.LogManager;

import project.common.master.AppConfig;

import com.sun.mail.smtp.SMTPAddressFailedException;
import com.sun.mail.smtp.SMTPAddressSucceededException;

import common.db.JdbcSource;
import common.db.jdbc.Jdbc;
import common.sql.QueryUtil;
import core.config.Factory;
import core.exception.PhysicalException;
import core.exception.ThrowableUtil;
import core.util.ArrayUtil;
import core.util.MojiUtil;

/**
 * メール送信クラス
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class EmailArticle {

	/** 題 */
	private String subject = null;
	/** 差出元アドレス */
	private String from = null;
	/** 宛先アドレス */
	private String to = null;
	/** カーボンコピーアドレス */
	private String cc = null;
	/** 返却アドレス */
	private String reply = null;
	/** Return-Path */
	private String path = null;
	/** メールテンプレート */
	private String[] body = null;
	/** ユーザID */
	private String user = null;
	/** パスワード */
	private String password = null;
	/** 題パラメータ */
	private String[] subjprm = null;
	/** 題マップ */
	private Map<String, Object> subjmap = null;
	/** 本文パラメータ */
	private String[] bodyprm = null;
	/** 本文マップ */
	private Map<String, Object> bodymap = null;

	/** メールサーバ */
	private final String server;

	/**
	 * コンストラクタ
	 */
	public EmailArticle() {
		AppConfig cfg = Factory.create(AppConfig.class);
		this.server = cfg.getValue("SYSTEM", "SERVER", "MAIL");
	}

	/**
	 * コンストラクタ
	 * @param sv メールサーバ
	 */
	public EmailArticle(final String sv) {
		this.server = sv;
	}

	/**
	 * メール可能文字列判断
	 * @param str 文字列
	 * @return メール可能の場合 true を返す。
	 */
	public static boolean isReadable(final String str) {
		byte[] bytes = str.getBytes(MojiUtil.CHARSET_XJIS);
		return str.equals(new String(bytes, MojiUtil.CHARSET_XJIS));
	}

	/**
	 * メール送信可能判断
	 * @return メール送信可能の場合 true を返す。
	 */
	public boolean isSendable() {
		return !Objects.toString(this.server, "").isEmpty()
				&& !Objects.toString(this.from, "").isEmpty()
				&& !Objects.toString(this.to, "").isEmpty()
				&& !Objects.toString(this.subject, "").isEmpty();
	}

	/**
	 * 題設定
	 * @param val 題
	 */
	public void setSubject(final String val) {
		this.subject = val;
	}

	/**
	 * 送信元取得
	 *
	 * @return 送信元
	 */
	public String getFrom() {
		return this.from;
	}

	/**
	 * 送信元設定
	 *
	 * @param val 送信元
	 */
	public void setFrom(final String val) {
		this.from = val;
	}

	/**
	 * 宛先取得
	 *
	 * @return 宛先
	 */
	public String getTo() {
		return this.to;
	}

	/**
	 * 宛先設定
	 *
	 * @param val 宛先
	 */
	public void setTo(final String val) {
		this.to = val;
	}

	/**
	 * 複写取得
	 *
	 * @return 複写
	 */
	public String getCc() {
		return this.cc;
	}

	/**
	 * 複写設定
	 *
	 * @param val 複写
	 */
	public void setCc(final String val) {
		this.cc = val;
	}

	/**
	 * 返信先取得
	 * @return 返信先
	 */
	public String getReplyTo() {
		return this.reply;
	}

	/**
	 * 返信先設定
	 * @param val 返信先
	 */
	public void setReplyTo(final String val) {
		this.reply = val;
	}

	/**
	 * Return-Path設定
	 * @param val Return-Path
	 */
	public void setReturnPath(final String val) {
		this.path = val;
	}

	/**
	 * 本文設定
	 * @param val 本文
	 */
	public void setBodyText(final String... val) {
		this.body = ArrayUtil.copyOf(val);
	}

	/**
	 * ユーザID設定
	 *
	 * @param val ユーザID
	 */
	public void setUser(final String val) {
		this.user = val;
	}

	/**
	 * パスワード設定
	 *
	 * @param val パスワード
	 */
	public void setPassword(final String val) {
		this.password = val;
	}

	/**
	 * 題パラメータ設定
	 *
	 * @param vals 題パラメータ
	 */
	public void setSubjectParam(final String... vals) {
		this.subjprm = null;
		if (vals != null) {
			this.subjprm = vals.clone();
		}
	}

	/**
	 * 題用パラメタマップ設定
	 * @param map 題用パラメタマップ
	 */
	public void setSubjectParamMap(final Map<String, Object> map) {
		this.subjmap = map;
	}

	/**
	 * 本文パラメータ設定
	 *
	 * @param vals 本文パラメータ
	 */
	public void setBodyParam(final String... vals) {
		this.bodyprm = null;
		if (vals != null) {
			this.bodyprm = vals.clone();
		}
	}

	/**
	 * 本文用パラメタマップ設定
	 * @param map 本文用パラメタマップ
	 */
	public void setBodyParamMap(final Map<String, Object> map) {
		this.bodymap = map;
	}

	/**
	 * 送信処理
	 *
	 * @return 送信成功の場合 true を返す。
	 */
	public boolean send() {
		if (this.subject != null) {
			this.subject = format(this.subject, this.subjmap, this.subjprm);

			try {
				// 送信
				Transport.send(getMessage(formatBody()));
				return true;
			} catch (final SMTPAddressSucceededException ex) {
				LogManager.getLogger().info(ex.getMessage());
				return true;
			} catch (final AddressException | SMTPAddressFailedException ex) {
				LogManager.getLogger().warn(ex.getMessage());
				LogManager.getLogger().warn(getTo());
				LogManager.getLogger().warn(getCc());
			} catch (final MessagingException | UnsupportedEncodingException ex) {
				LogManager.getLogger().error(ex.getMessage(), ex);
			}
		}
		return false;
	}

	/**
	 * メール情報設定
	 * @param id メール制御ID
	 * @return EmailArticle
	 */
	public EmailArticle setMailInfo(final String id) {
		if (id != null) {
			try (Jdbc conn = JdbcSource.getConnection()) {
				setMailCtrl(id, conn);
				setMailText(id, conn);
			}
		}
		return this;
	}

	/**
	 * フォーマット処理
	 * @param str フォーマット対象文字列
	 * @param map パラメタマップ
	 * @param prm ぱらめた
	 * @return フォーマット済み文字列
	 */
	private String format(final String str, final Map<String, Object> map, final String... prm) {
		if (prm != null) {
			return new MessageFormat(str).format(prm);
		} else if (map != null) {
			return fillIn(str, map);
		}
		return str;
	}

	/**
	 * 本文埋込
	 * @return 本文
	 */
	private String formatBody() {
		StringBuilder sb = new StringBuilder();
		if (this.body != null) {
			for (final String str : this.body) {
				if (str != null) {
					sb.append(format(str, this.bodymap, this.bodyprm));
				}
				sb.append("\r\n");
			}
		}
		return sb.toString();
	}

	/**
	 * パス内の{}で括られた文字列をモデル内の値で置換する。
	 * @param str 置換対象文字列
	 * @param map パラメタマップ
	 * @return 置換後文字列
	 */
	private String fillIn(final String str, final Map<String, Object> map) {
		if (str != null) {
			int open = str.indexOf('{');
			int close = str.indexOf('}', open);
			if (0 <= open && open < close) {
				return str.substring(0, open)
					+ map.getOrDefault(str.substring(open + "{".length(), close), "")
					+ fillIn(str.substring(close + "}".length()), map);
			}
		}
		return str;
	}

	/**
	 * Email取得
	 *
	 * @param str 本文
	 * @return Email
	 * @throws MessagingException メッセージ例外
	 * @throws UnsupportedEncodingException エンコード例外
	 */
	private Message getMessage(final String str)
					throws MessagingException, UnsupportedEncodingException {
		if (Objects.toString(this.server, "").isEmpty()) {
			throw new MessagingException("Mail Server is Empty.");
		}

		// メール送信準備
		Properties props = new Properties();
		props.put("mail.host", this.server);
		props.put("mail.smtp.host", this.server);
		// 送信可能アドレスには送るように
		props.put("mail.smtp.sendpartial", "true");
		// 認証
		Authenticator auth = null;
		if (this.user != null) {
			auth = new Auth(this.user, this.password);
			props.setProperty("mail.smtp.auth", "true");
			props.setProperty("mail.smtp.port", "587");
			props.setProperty("mail.smtp.starttls.enable", "true");
		}

		// メール作成
		MimeMessage msg = new MimeMessage(Session.getInstance(props, auth));

		msg.addFrom(toPersonalAddress(this.from));
		msg.setRecipients(Message.RecipientType.TO, toPersonalAddress(this.to));
		if (this.cc != null) {
			msg.setRecipients(Message.RecipientType.CC, toPersonalAddress(this.cc));
		}
		if (this.reply != null) {
			msg.setReplyTo(toPersonalAddress(this.reply));
		}

		setSubject(msg);

		if (this.path != null) {
			msg.setHeader("Return-Path", this.path);
		}

		msg.setDataHandler(new DataHandler(new JisDataSource(str)));
		msg.setHeader("Content-Transfer-Encoding", "7bit");

		return msg;
	}

	/**
	 * 題名設定
	 *
	 * @param msg メッセージオブジェクト
	 * @throws MessagingException メッセージ例外
	 */
	private void setSubject(final Message msg) throws MessagingException {
		try {
			String xjis = MojiUtil.CHARSET_XJIS.name();
			String sbj = MimeUtility.encodeText(this.subject, xjis, null);
			sbj = sbj.replace(xjis, MojiUtil.CHARSET_JIS.name());
			msg.setHeader("Subject", MimeUtility.fold("Subject: ".length(), sbj));
		} catch (final UnsupportedEncodingException ex) {
			throw new IllegalStateException(ex);
		}
	}

	/**
	 * メール制御設定
	 *
	 * @param id メール制御ID
	 * @param conn コネクション
	 */
	private void setMailCtrl(final String id, final Connection conn) {
		String query = QueryUtil.getSqlFromFile("SelectMailCtl", this.getClass());
		try (PreparedStatement psmt =
				QueryUtil.statementReadonly(conn, query, Collections.singletonMap("CtlId", id))) {
			try (ResultSet rs = psmt.executeQuery()) {
				if (rs.next()) {
					this.subject = rs.getString("MAIL_SUBJECT_NAME");
					this.from = rs.getString("FROM_MAIL_ADDRESS");
					this.to = rs.getString("TO_MAIL_ADDRESS");
					this.cc = rs.getString("CC_MAIL_ADDRESS");
					this.reply = rs.getString("REPLY_TO_MAIL_ADDRESS");
				}
			}
		} catch (final SQLException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * 本文配列取得
	 *
	 * @param id メール制御ID
	 * @param conn コネクション
	 */
	private void setMailText(final String id, final Connection conn) {

		String query = QueryUtil.getSqlFromFile("SelectMailText", this.getClass());
		try (PreparedStatement psmt =
				QueryUtil.statementReadonly(conn, query, Collections.singletonMap("CtlId", id))) {
			List<String> list = new ArrayList<>();
			try (ResultSet rs = psmt.executeQuery()) {
				while (rs.next()) {
					list.add(rs.getString("MAIL_TEXT"));
				}
			}
			this.body = list.toArray(new String[list.size()]);

		} catch (final SQLException ex) {
			ThrowableUtil.error(ex);
			throw new PhysicalException(ex);
		}
	}

	/**
	 * アドレス化
	 * @param addresses アドレス
	 * @return アドレス
	 * @throws UnsupportedEncodingException エンコード例外
	 * @throws AddressException アドレス例外
	 */
	private InternetAddress[] toPersonalAddress(final String addresses)
					throws UnsupportedEncodingException, AddressException {
		String[] address = addresses.split(",");
		InternetAddress[] ret = new InternetAddress[addresses.length()];
		for (int i = 0; i < ret.length; i++) {
			int loc = address[i].lastIndexOf(':');
			if (0 <= loc) {
				ret[i] = new InternetAddressEx(address[i].substring(0, loc),
						address[i].substring(loc + ":".length()), MojiUtil.CHARSET_XJIS);
			} else {
				ret[i] = new InternetAddressEx(address[i]);
			}
		}
		return ret;
	}

	/**
	 * InternetAddress拡張
	 * @author Tadashi Nakayama
	 */
	private static final class InternetAddressEx extends InternetAddress {
		/** serialVersionUID */
		private static final long serialVersionUID = 8583837194606507146L;

		/**
		 * コンストラクタ
		 * @param ads アドレス
		 * @throws AddressException アドレス例外
		 */
		public InternetAddressEx(final String ads) throws AddressException {
			super(ads);
		}

		/**
		 * コンストラクタ
		 * @param ads アドレス
		 * @param psn 人物
		 * @param charset エンコード
		 * @throws UnsupportedEncodingException 例外
		 */
		public InternetAddressEx(final String ads, final String psn,
						final Charset charset) throws UnsupportedEncodingException {
			super(ads, psn, charset.name());
			if (super.encodedPersonal != null) {
				super.encodedPersonal = super.encodedPersonal.replace(
						MojiUtil.CHARSET_XJIS.name(), MojiUtil.CHARSET_JIS.name());
			}
		}
	}

	/**
	 * 認証クラス
	 *
	 * @author Tadashi Nakayama
	 * @version 1.0.0
	 */
	private static final class Auth extends Authenticator {
		/** ユーザID */
		private final String user;
		/** パスワード */
		private final String password;

		/**
		 * コンストラクタ
		 *
		 * @param u ユーザID
		 * @param p パスワード
		 */
		public Auth(final String u, final String p) {
			this.user = u;
			this.password = p;
		}

		/**
		 * @see javax.mail.Authenticator#getPasswordAuthentication()
		 */
		@Override
		protected PasswordAuthentication getPasswordAuthentication() {
			return new PasswordAuthentication(this.user, this.password);
		}
	}

	/**
	 * データソース
	 *
	 * @author Tadashi Nakayama
	 * @version 1.0.0
	 */
	private static final class JisDataSource implements DataSource {
		/** 文字列 */
		private final String data;

		/**
		 * コンストラクタ
		 *
		 * @param str 文字列
		 */
		public JisDataSource(final String str) {
			this.data = str;
		}

		/**
		 * @see javax.activation.DataSource#getContentType()
		 */
		@Override
		public String getContentType() {
			return "text/plain; charset=iso-2022-jp";
		}

		/**
		 * @see javax.activation.DataSource#getInputStream()
		 */
		@Override
		public InputStream getInputStream() throws IOException {
			return new ByteArrayInputStream(this.data.getBytes(MojiUtil.CHARSET_XJIS));
		}

		/**
		 * @see javax.activation.DataSource#getName()
		 */
		@Override
		public String getName() {
			return this.getClass().getName();
		}

		/**
		 * @see javax.activation.DataSource#getOutputStream()
		 */
		@Override
		public OutputStream getOutputStream() throws IOException {
			throw new UnsupportedOperationException();
		}
	}
}
